﻿/*!
 * Copyright (c), 上海沃恩信息科技有限公司.
 * All rights reserved.
 * Licensed under BSD (https://www.pittypat.work/bsd.html).
 */

using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.WebSockets;
using System.Threading.Tasks;

namespace Pittypat.Web
{
    /// <summary>
    /// 表示由虚拟路径定义的文件。
    /// </summary>
    /// <remarks>
    /// 可以用于在功能的 Api 中作为返回值向客户端发送已知虚拟路径的文件。
    /// </remarks>
    public sealed class VirtualFile : IJson
    {
        /// <summary>
        /// 获取指定虚拟路径对应的物理路径。
        /// </summary>
        /// <param name="virtualPath">虚拟路径，可以使用 "~/" 前缀。</param>
        /// <returns>返回物理路径。如果 virtualPath 为 null 或空字符串，则返回空字符串。</returns>
        public static string MapPath(string virtualPath)
        {
            if (string.IsNullOrEmpty(virtualPath))
            {
                return "";
            }

            return System.Web.HttpContext.Current.Server.MapPath(virtualPath);
        }

        /// <summary>
        /// 删除指定虚拟路径上的一个文件。
        /// </summary>
        /// <param name="virtualPath">要删除的文件的虚拟路径。</param>
        /// <param name="ignoreError">指示是否应该忽略在操作过程中产生的任何异常。</param>
        /// <exception cref="ArgumentException">ignoreError 为 false，并且没有提供虚拟路径。</exception>
        public static void Delete(string virtualPath, bool ignoreError = true)
        {
            if (ignoreError)
            {
                if (!string.IsNullOrEmpty(virtualPath))
                {
                    try
                    {
                        File.Delete(MapPath(virtualPath));
                    }
                    catch
                    {
                    }
                }
            }
            else
            {
                if (string.IsNullOrEmpty(virtualPath))
                {
                    throw new ArgumentException("没有提供路径。", "virtualPath");
                }

                File.Delete(MapPath(virtualPath));
            }
        }

        private string virtualPath;
        private string fileName;
        private string contentType;
        private string physicalPath;

        /// <summary>
        /// 使用虚拟路径、文件名称和文件类型初始化 <see cref="VirtualFile"/> 的新实例。
        /// </summary>
        /// <param name="virtualPath">虚拟路径，可以使用 "~/"前缀。</param>
        /// <param name="fileName">重命名的文件名称，如果包含扩展名称，则使用此扩展名称推测文件类型。</param>
        /// <param name="contentType">文件的 MIME 类型，如果没有提供，则根据文件的扩展名称自动推测。</param>
        public VirtualFile(string virtualPath, string fileName = null, string contentType = null)
        {
            if (string.IsNullOrEmpty(virtualPath))
            {
                throw new ArgumentException("文件的虚拟路径不能是空的。", "virtualPath");
            }

            this.physicalPath = MapPath(virtualPath);
            this.fileName = string.IsNullOrEmpty(fileName) ? Path.GetFileName(this.physicalPath) : fileName;
            this.contentType = string.IsNullOrEmpty(contentType) ? Mime.Of(Path.GetExtension(this.fileName)) : contentType;
            this.virtualPath = virtualPath;
        }

        /// <summary>
        /// 获取文件的虚拟路径。
        /// </summary>
        public string VirtualPath
        {
            get
            {
                return this.virtualPath;
            }
        }

        /// <summary>
        /// 获取文件的物理路径。
        /// </summary>
        public string PhysicalPath
        {
            get
            {
                return this.physicalPath;
            }
        }

        /// <summary>
        /// 获取文件名称。
        /// </summary>
        public string FileName
        {
            get
            {
                return this.fileName;
            }
        }

        /// <summary>
        /// 获取文件的类型。
        /// </summary>
        public string ContentType
        {
            get
            {
                return this.contentType;
            }
        }

        /// <summary>
        /// 将当前对象作为 JSON 字符串写入指定的文本流。
        /// </summary>
        /// <param name="writer">目标文本流。</param>
        void IJson.Write(TextWriter writer)
        {
            writer.Write("{{\"url\":\"{0}\",\"fileName\":\"{1}\",\"contentType\":\"{2}\"}}",
                Json.Escape(this.virtualPath), Json.Escape(this.fileName), Json.Escape(this.contentType));
        }

        /// <summary>
        /// 返回文件的虚拟路径字符串。
        /// </summary>
        /// <returns>文件的虚拟路径字符串。</returns>
        public override string ToString()
        {
            return this.virtualPath;
        }

        /// <summary>
        /// 通过 WebSocket 发送文件。
        /// </summary>
        /// <param name="progress">汇报异步操作的进度。</param>
        /// <param name="bufferSize">发送缓冲区的尺寸。</param>
        /// <returns>总共发送的字节数。</returns>
        internal async Task<long> SendAsync(WebSocketAsyncProgress progress, int bufferSize = 0)
        {
            // 缓冲区尺寸，如果没有提供参数 bufferSize，则使用配置文件中的缓冲区尺寸
            bufferSize = bufferSize <= 0 ? WebSocketHelper.MaxBufferSize : bufferSize;

            // 发送数据的缓冲区
            var buffer = new byte[bufferSize];

            // 创建用于读取文件的文件流
            using (var stream = new FileStream(this.physicalPath, FileMode.Open, FileAccess.Read))
            {
                var total = stream.Length;
                var sent = 0L;
                var read = 0;

                while (sent < total)
                {
                    // 从流中异步读取数据
                    read = await stream.ReadAsync(buffer, 0, bufferSize, progress.Token);
                    if (read <= 0)
                    {
                        // 没有读取到任何数据
                        break;
                    }

                    // 增加读取计数
                    sent += read;

                    // 发送刚刚读取到的数据
                    await progress.Socket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Binary, sent >= total, progress.Token);
                }

                return sent;
            }
        }

        /// <summary>
        /// 将文件作为响应内容附加到指定的响应消息。
        /// </summary>
        /// <param name="response">目标响应对象。</param>
        /// <remarks>
        /// 在内部创建新的文件流，并保持打开状态。调用房负责关闭和释放文件流。
        /// </remarks>
        internal void ToResponseContent(HttpResponseMessage response)
        {
            var stream = new FileStream(this.physicalPath, FileMode.Open, FileAccess.Read);
            response.Content = new StreamContent(stream);
            response.Content.Headers.ContentLength = stream.Length;
            response.Content.Headers.ContentType = new MediaTypeHeaderValue(this.contentType);
            response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
            {
                FileName = this.fileName
            };
        }
    }
}
